﻿using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Reflection;

using BizElements.TemplateEngine;

namespace BizElements.Generator
{
    sealed class DotNetProjectGeneratorProperties : ICodeGeneratorProperties
    {
        #region ICodeGeneratorProperties Properties.

        public string[] SelectedItems { get; set; }
        public ITemplate[] ItemTemplates { get; set; }
        public string OutputFolder { get; set; }
        public IDictionary<string, IDictionary<string, string>> AllComponentVariables { get; set; }        

        #endregion

        #region .NET specific properties.

        public Assembly LoadedAssembly { get; set; }

        #endregion

        #region Validate.

        public string[] Validate()
        {
            List<string> errors = new List<string>();
            if (this.LoadedAssembly == null)
                errors.Add(Messages.DotNetProjectGeneratorProperties_LoadedAssemblyRequired);

            if (this.SelectedItems == null || this.SelectedItems.Length == 0)
                errors.Add(Messages.DotNetProjectGeneratorProperties_ListOfSelectedClassesIsNull);

            if (this.ItemTemplates == null || this.ItemTemplates.Length == 0)
                errors.Add(Messages.DotNetProjectGeneratorProperties_ItemTemplates_AtLeastOneRequired);

            if (string.IsNullOrEmpty(OutputFolder))
                errors.Add(Messages.DotNetProjectGeneratorProperties_OutputFolderMustBeDefined);

            return errors.ToArray();
        }

        #endregion
    }    

    sealed class DotNetProjectGenerator : ICodeGenerator
    {        
        const double ObjectCreationCostPct = 10.0;
        const double MultiFileTemplatesCostPct = 80.0;
        const double SingleFileTemplatesCostPct = 10.0;

        #region Fields.

        bool stopGenerator;
        string[] selectedItems;
        ITemplate[] itemTemplates;
        string outputFolder;
        Assembly asm;

        #endregion

        #region CTor.

        public DotNetProjectGenerator(DotNetProjectGeneratorProperties properties)
        {
            this.selectedItems = properties.SelectedItems;
            this.itemTemplates = properties.ItemTemplates;
            this.outputFolder = TextUtil.EnsureSuffix(properties.OutputFolder, @"\");
            this.asm = properties.LoadedAssembly;
            foreach (ITemplate template in this.itemTemplates)
                template.AllComponentVariables = properties.AllComponentVariables;
        }

        #endregion

        #region Events.

        public event EventHandler<EventArgs<GenerationTraceItem>> ItemGenerated;

        private void FireItemGenerated(string message, int progresPercentage)
        {
            if (this.ItemGenerated != null)
                this.ItemGenerated(this, new EventArgs<GenerationTraceItem>(new GenerationTraceItem(message ?? "", progresPercentage)));
        }

        public event EventHandler<EventArgs<GenerationTraceItem>> GenerationCompleted;

        private void FireGenerationCompleted()
        {
            if (this.GenerationCompleted != null)
                this.GenerationCompleted(this, new EventArgs<GenerationTraceItem>(new GenerationTraceItem("", 100)));
        }

        #endregion

        public void StopGenerator()
        {
            // This can be called from another thread.
            this.stopGenerator = true;
        }

        #region GenerateCode.

        public void GenerateCode()
        {
            try
            {
                GenerateAllCode();
            }
            finally
            {
                FireGenerationCompleted();
            }
        }

        private void GenerateAllCode()
        {
            this.stopGenerator = false;
            FireItemGenerated(Messages.DotNetProjectGenerator_CodeGenerationStarted, 0);
            DateTime start = DateTime.Now;

            CreateOutputFolders();
            object[] bizObjects = InstantiateObjects();
            if ((bizObjects == null) || (bizObjects.Length == 0))
                return;

            bool multiFileTemplatesOk = GenerateFilesForObjects(bizObjects);
            if (!multiFileTemplatesOk)
                return;

            bool singeFileTemplatesOk = GenerateSingleFileTemplatesForObjects(bizObjects);
            if (!singeFileTemplatesOk)
                return;

            string msgTotalStats = string.Format(Messages.DotNetProjectGenerator_CompletedTimeUsedX, GenUtil.GetDuration(start));
            FireItemGenerated(msgTotalStats, 100);
        }

        #endregion

        #region Create folders.

        private void CreateOutputFolders()
        {
            CreateDirectory(this.outputFolder);
            foreach (ITemplate tblItemTemplate in this.itemTemplates)
            {
                string subdir = GenUtil.GetOutputSubdirectoryVariable(tblItemTemplate);
                if (!string.IsNullOrEmpty(subdir))
                    CreateDirectory(this.outputFolder + subdir);
            }
        }

        private void CreateDirectory(string path)
        {
            if (!Directory.Exists(path))
            {
                Directory.CreateDirectory(path);
                string msg = string.Format(Messages.DotNetProjectGenerator_DirectoryXCreated, path);
                FireItemGenerated(msg, 0);
            }
        }        

        #endregion

        #region InstantiateObjects.

        private object[] InstantiateObjects()
        {
            // Temporary switch directory.
            string oldCwd = Directory.GetCurrentDirectory();
            string oldAsmDir = AppMain.AsmDependenciesDirectory;
            string asmPath = this.asm.Location;
            string tempCwd = Path.GetDirectoryName(asmPath);
            Directory.SetCurrentDirectory(tempCwd);
            AppMain.AsmDependenciesDirectory = tempCwd;

            try
            {
                List<object> bizObjects = new List<object>();
                double i = 0;
                foreach (string className in this.selectedItems)
                {
                    int pctProgress = Convert.ToInt32(ObjectCreationCostPct * i / this.selectedItems.Length);
                    if (this.stopGenerator)
                    {
                        FireItemGenerated(Messages.DotNetProjectGenerator_GeneratorStopped, pctProgress);
                        return null;
                    }

                    string error;
                    object obj = TryCreateInstance(this.asm, className, out error);
                    if (obj != null)
                    {
                        bizObjects.Add(obj);
                        string msg = string.Format(Messages.DotNetProjectGenerator_CreatedObjectX, className);
                        FireItemGenerated(msg, pctProgress);
                    }
                    else
                    {
                        FireItemGenerated(error, pctProgress);
                    }

                    i++;
                }

                return bizObjects.ToArray();
            }
            finally
            {
                Directory.SetCurrentDirectory(oldCwd);
                AppMain.AsmDependenciesDirectory = oldAsmDir;
            }
        }

        private static object TryCreateInstance(Assembly asm, string className, out string error)
        {
            object instance = null;
            try
            {
                var ctor = asm.GetType(className).GetConstructor(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance, null, new Type[0], new ParameterModifier[0]);
                if (ctor == null)
                {
                    error = string.Format(Messages.DotNetProjecGenerator_TypeXDoesntHaveDefaultCtor, className);
                }
                else
                {
                    instance = ctor.Invoke(null);
                    if (instance == null)
                        error = string.Format(Messages.DotNetProjectGenerator_CannotInstantiateTypeX, className);
                    else
                        error = "";
                }
            }
            catch (MissingMethodException)
            {
                error = string.Format(Messages.DotNetProjecGenerator_TypeXDoesntHaveDefaultCtor, className);
            }
            catch (TargetInvocationException tie)
            {
                error = string.Format(Messages.DotNetProjectGenerator_CannotInstantiateTypeXErrorY, className, tie.InnerException.ToString());
            }
            catch (Exception otherExc)
            {
                error = string.Format(Messages.DotNetProjectGenerator_CannotInstantiateTypeXErrorY, className, otherExc.ToString());
            }

            return instance;
        }

        #endregion

        #region GenerateFilesForObjects - each template generates one file per object.

        private bool GenerateFilesForObjects(object[] bizObjects)
        {
            List<ITemplate> multiFileTemplates = GenUtil.GetMultiFileTemplates(this.itemTemplates);
            if (multiFileTemplates.Count == 0)
                return true;

            DateTime start = DateTime.Now;
            int percentage = Convert.ToInt32(ObjectCreationCostPct);
            double i = 0;
            foreach (object obj in bizObjects)
            {
                percentage = Convert.ToInt32(ObjectCreationCostPct + MultiFileTemplatesCostPct * i / bizObjects.Length);
                if (this.stopGenerator)
                {
                    FireItemGenerated(Messages.DotNetProjectGenerator_GeneratorStopped, percentage);
                    return false;
                }

                GenerateFilesForObject(obj, multiFileTemplates);
                string msg = string.Format(Messages.DotNetProjectGenerator_GeneratedCodeForClassX, obj.GetType().FullName);
                FireItemGenerated(msg, percentage);
                i++;
            }

            string msgStatistics = string.Format(Messages.DotNetProjectGenerator_GeneratedCodeForAllClassesTimesUsedX, GenUtil.GetDuration(start));
            FireItemGenerated(msgStatistics, percentage);
            return true;
        }

        private void GenerateFilesForObject(object obj, List<ITemplate> multiFileTemplates)
        {
            foreach (ITemplate template in multiFileTemplates)
            {
                if (template.IsValidInputComponent(obj))
                {
                    string classFileName = GenUtil.CreateClassOutputFilePath(null, obj.GetType(), template, this.outputFolder);
                    using (StreamWriter classOutput = new StreamWriter(classFileName, /*overwrite existing file*/ false, Encoding.UTF8))
                    {
                        template.Run(classOutput, obj);
                    }
                }
            }
        }

        #endregion

        #region GenerateSingleFileTemplatesForObjects - template generates single file that contains code for all objects/items.

        private bool GenerateSingleFileTemplatesForObjects(object[] bizObjects)
        {
            List<ITemplate> singleFileTemplates = GenUtil.GetSingleFileTemplates(this.itemTemplates);
            if (singleFileTemplates.Count == 0)
                return true;

            int percentage = Convert.ToInt32(ObjectCreationCostPct + MultiFileTemplatesCostPct);
            double i = 0;
            foreach (ITemplate template in singleFileTemplates)
            {
                percentage = Convert.ToInt32(ObjectCreationCostPct + MultiFileTemplatesCostPct + SingleFileTemplatesCostPct * i / singleFileTemplates.Count);
                if (this.stopGenerator)
                {
                    FireItemGenerated(Messages.DotNetProjectGenerator_GeneratorStopped, percentage);
                    return false;
                }

                string generatedFileName = GenerateSingleFileForTemplate(template, bizObjects);
                string msg = string.Format(Messages.DotNetProjectGenerator_GeneratedFileX, generatedFileName);
                FireItemGenerated(msg, percentage);
            }

            return true;
        }

        private string GenerateSingleFileForTemplate(ITemplate template, object[] bizObjects)
        {
            string fileNameWithExt = GenUtil.GetSingleFieldVariableOrSetting(template);
            string filePath = GenUtil.CreateSingleFilePathForTemplate(template, this.outputFolder, fileNameWithExt);
            using (StreamWriter codeOutput = new StreamWriter(filePath, /*overwrite existing file*/ false, Encoding.UTF8))
            {
                template.Run(codeOutput, bizObjects);
            }

            return fileNameWithExt;            
        }

        #endregion
    }
}
